package com.hutgin2.inject.hibernate.hack;

import org.hibernate.AssertionFailure;
import org.hibernate.EntityMode;
import org.hibernate.cfg.NotYetImplementedException;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.beans.BeanInfoHelper;
import org.hibernate.metamodel.binding.*;
import org.hibernate.metamodel.domain.Component;
import org.hibernate.metamodel.domain.Entity;
import org.hibernate.metamodel.domain.PluralAttribute;
import org.hibernate.metamodel.domain.SingularAttribute;
import org.hibernate.metamodel.relational.*;
import org.hibernate.metamodel.source.LocalBindingContext;
import org.hibernate.metamodel.source.MappingException;
import org.hibernate.metamodel.source.MetaAttributeContext;
import org.hibernate.metamodel.source.MetadataImplementor;
import org.hibernate.metamodel.source.binder.*;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.tuple.entity.EntityTuplizer;

import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Kind of hack
 * {@link org.hibernate.metamodel.source.binder.Binder}
 * TODO review
 */
public class DMBinder {
    private final MetadataImplementor metadata;
    private final List<String> processedEntityNames;

    private InheritanceType currentInheritanceType;
    private EntityMode currentHierarchyEntityMode;
    private LocalBindingContext currentBindingContext;

    public DMBinder(MetadataImplementor metadata, List<String> processedEntityNames) {
        this.metadata = metadata;
        this.processedEntityNames = processedEntityNames;
    }

    /**
     * Process an entity hierarchy.
     *
     * @param entityHierarchy THe hierarchy to process.
     */
    public void processEntityHierarchy(EntityHierarchy entityHierarchy) {
        currentInheritanceType = entityHierarchy.getHierarchyInheritanceType();
        EntityBinding rootEntityBinding = createEntityBinding(entityHierarchy.getRootEntitySource(), null);
        if (currentInheritanceType != InheritanceType.NO_INHERITANCE) {
            processHierarchySubEntities(entityHierarchy.getRootEntitySource(), rootEntityBinding);
        }
        currentHierarchyEntityMode = null;
    }

    private void processHierarchySubEntities(SubclassEntityContainer subclassEntitySource, EntityBinding superEntityBinding) {
        for (SubclassEntitySource subEntity : subclassEntitySource.subclassEntitySources()) {
            EntityBinding entityBinding = createEntityBinding(subEntity, superEntityBinding);
            processHierarchySubEntities(subEntity, entityBinding);
        }
    }


    // Entities ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    private EntityBinding createEntityBinding(EntitySource entitySource, EntityBinding superEntityBinding) {
        if (processedEntityNames.contains(entitySource.getEntityName())) {
            return metadata.getEntityBinding(entitySource.getEntityName());
        }

        currentBindingContext = entitySource.getLocalBindingContext();
        try {
            final EntityBinding entityBinding = doCreateEntityBinding(entitySource, superEntityBinding);

            metadata.addEntity(entityBinding);
            processedEntityNames.add(entityBinding.getEntity().getName());

            processFetchProfiles(entitySource, entityBinding);

            return entityBinding;
        } finally {
            currentBindingContext = null;
        }
    }

    private EntityBinding doCreateEntityBinding(EntitySource entitySource, EntityBinding superEntityBinding) {
        final EntityBinding entityBinding = createBasicEntityBinding(entitySource, superEntityBinding);

        bindSecondaryTables(entitySource, entityBinding);
        bindAttributes(entitySource, entityBinding);

        bindTableUniqueConstraints(entitySource, entityBinding);

        return entityBinding;
    }

    private EntityBinding createBasicEntityBinding(EntitySource entitySource, EntityBinding superEntityBinding) {
        if (superEntityBinding == null) {
            return makeRootEntityBinding((RootEntitySource) entitySource);
        } else {
            switch (currentInheritanceType) {
                case SINGLE_TABLE:
                    return makeDiscriminatedSubclassBinding((SubclassEntitySource) entitySource, superEntityBinding);
                case JOINED:
                    return makeJoinedSubclassBinding((SubclassEntitySource) entitySource, superEntityBinding);
                case TABLE_PER_CLASS:
                    return makeUnionedSubclassBinding((SubclassEntitySource) entitySource, superEntityBinding);
                default:
                    // extreme internal error!
                    throw new AssertionFailure("Internal condition failure");
            }
        }
    }

    private EntityBinding makeRootEntityBinding(RootEntitySource entitySource) {
        currentHierarchyEntityMode = entitySource.getEntityMode();

        final EntityBinding entityBinding = buildBasicEntityBinding(entitySource, null);

        bindPrimaryTable(entitySource, entityBinding);

        bindIdentifier(entitySource, entityBinding);
        bindVersion(entityBinding, entitySource);
        bindDiscriminator(entitySource, entityBinding);

        entityBinding.getHierarchyDetails().setCaching(entitySource.getCaching());
        entityBinding.getHierarchyDetails().setExplicitPolymorphism(entitySource.isExplicitPolymorphism());
        entityBinding.getHierarchyDetails().setOptimisticLockStyle(entitySource.getOptimisticLockStyle());

        entityBinding.setMutable(entitySource.isMutable());
        entityBinding.setWhereFilter(entitySource.getWhere());
        entityBinding.setRowId(entitySource.getRowId());

        return entityBinding;
    }

    private EntityBinding buildBasicEntityBinding(EntitySource entitySource, EntityBinding superEntityBinding) {
        final EntityBinding entityBinding = superEntityBinding == null
                ? new EntityBinding(currentInheritanceType, currentHierarchyEntityMode)
                : new EntityBinding(superEntityBinding);

        final String entityName = entitySource.getEntityName();
        final String className = currentHierarchyEntityMode == EntityMode.POJO ? entitySource.getClassName() : null;

        // HACK
        final Entity entity = new DMEntity(
                entityName,
                className,
                currentBindingContext.makeClassReference(className),
                superEntityBinding == null ? null : superEntityBinding.getEntity()
        );
        // #HACK
        entityBinding.setEntity(entity);

        entityBinding.setJpaEntityName(entitySource.getJpaEntityName());

        if (currentHierarchyEntityMode == EntityMode.POJO) {
            final String proxy = entitySource.getProxy();
            if (proxy != null) {
                entityBinding.setProxyInterfaceType(
                        currentBindingContext.makeClassReference(
                                currentBindingContext.qualifyClassName(proxy)
                        )
                );
                entityBinding.setLazy(true);
            } else if (entitySource.isLazy()) {
                entityBinding.setProxyInterfaceType(entityBinding.getEntity().getClassReferenceUnresolved());
                entityBinding.setLazy(true);
            }
        } else {
            entityBinding.setProxyInterfaceType(null);
            entityBinding.setLazy(entitySource.isLazy());
        }

        final String customTuplizerClassName = entitySource.getCustomTuplizerClassName();
        if (customTuplizerClassName != null) {
            entityBinding.setCustomEntityTuplizerClass(
                    currentBindingContext.<EntityTuplizer>locateClassByName(
                            customTuplizerClassName
                    )
            );
        }

        final String customPersisterClassName = entitySource.getCustomPersisterClassName();
        if (customPersisterClassName != null) {
            entityBinding.setCustomEntityPersisterClass(
                    currentBindingContext.<EntityPersister>locateClassByName(
                            customPersisterClassName
                    )
            );
        }

        entityBinding.setMetaAttributeContext(buildMetaAttributeContext(entitySource));

        entityBinding.setDynamicUpdate(entitySource.isDynamicUpdate());
        entityBinding.setDynamicInsert(entitySource.isDynamicInsert());
        entityBinding.setBatchSize(entitySource.getBatchSize());
        entityBinding.setSelectBeforeUpdate(entitySource.isSelectBeforeUpdate());
        entityBinding.setAbstract(entitySource.isAbstract());

        entityBinding.setCustomLoaderName(entitySource.getCustomLoaderName());
        entityBinding.setCustomInsert(entitySource.getCustomSqlInsert());
        entityBinding.setCustomUpdate(entitySource.getCustomSqlUpdate());
        entityBinding.setCustomDelete(entitySource.getCustomSqlDelete());

        if (entitySource.getSynchronizedTableNames() != null) {
            entityBinding.addSynchronizedTableNames(entitySource.getSynchronizedTableNames());
        }

        entityBinding.setJpaCallbackClasses(entitySource.getJpaCallbackClasses());

        return entityBinding;
    }

    private EntityBinding makeDiscriminatedSubclassBinding(SubclassEntitySource entitySource, EntityBinding superEntityBinding) {
        final EntityBinding entityBinding = buildBasicEntityBinding(entitySource, superEntityBinding);

        entityBinding.setPrimaryTable(superEntityBinding.getPrimaryTable());
        entityBinding.setPrimaryTableName(superEntityBinding.getPrimaryTableName());
        bindDiscriminatorValue(entitySource, entityBinding);

        return entityBinding;
    }

    private EntityBinding makeJoinedSubclassBinding(SubclassEntitySource entitySource, EntityBinding superEntityBinding) {
        final EntityBinding entityBinding = buildBasicEntityBinding(entitySource, superEntityBinding);

        bindPrimaryTable(entitySource, entityBinding);

        // todo : join

        return entityBinding;
    }

    private EntityBinding makeUnionedSubclassBinding(SubclassEntitySource entitySource, EntityBinding superEntityBinding) {
        final EntityBinding entityBinding = buildBasicEntityBinding(entitySource, superEntityBinding);

        bindPrimaryTable(entitySource, entityBinding);

        // todo : ??

        return entityBinding;
    }

    // Attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    private void bindIdentifier(RootEntitySource entitySource, EntityBinding entityBinding) {
        if (entitySource.getIdentifierSource() == null) {
            throw new AssertionFailure("Expecting identifier information on root entity descriptor");
        }
        switch (entitySource.getIdentifierSource().getNature()) {
            case SIMPLE: {
                bindSimpleIdentifier((SimpleIdentifierSource) entitySource.getIdentifierSource(), entityBinding);
                break;
            }
            case AGGREGATED_COMPOSITE: {
                // composite id with an actual component class
                break;
            }
            case COMPOSITE: {
                // what we used to term an "embedded composite identifier", which is not tobe confused with the JPA
                // term embedded. Specifically a composite id where there is no component class, though there may
                // be a @IdClass :/
                break;
            }
        }
    }

    private void bindSimpleIdentifier(SimpleIdentifierSource identifierSource, EntityBinding entityBinding) {
        final BasicAttributeBinding idAttributeBinding = doBasicSingularAttributeBindingCreation(
                identifierSource.getIdentifierAttributeSource(), entityBinding
        );

        entityBinding.getHierarchyDetails().getEntityIdentifier().setValueBinding(idAttributeBinding);
        IdGenerator generator = identifierSource.getIdentifierGeneratorDescriptor();
        if (generator == null) {
            Map<String, String> params = new HashMap<String, String>();
            params.put(IdentifierGenerator.ENTITY_NAME, entityBinding.getEntity().getName());
            generator = new IdGenerator("default_assign_identity_generator", "assigned", params);
        }
        entityBinding.getHierarchyDetails()
                .getEntityIdentifier()
                .setIdGenerator(generator);

        final org.hibernate.metamodel.relational.Value relationalValue = idAttributeBinding.getValue();

        if (SimpleValue.class.isInstance(relationalValue)) {
            if (!Column.class.isInstance(relationalValue)) {
                // this should never ever happen..
                throw new AssertionFailure("Simple-id was not a column.");
            }
            entityBinding.getPrimaryTable().getPrimaryKey().addColumn(Column.class.cast(relationalValue));
        } else {
            for (SimpleValue subValue : ((Tuple) relationalValue).values()) {
                if (Column.class.isInstance(subValue)) {
                    entityBinding.getPrimaryTable().getPrimaryKey().addColumn(Column.class.cast(subValue));
                }
            }
        }
    }

    private void bindVersion(EntityBinding entityBinding, RootEntitySource entitySource) {
        final SingularAttributeSource versioningAttributeSource = entitySource.getVersioningAttributeSource();
        if (versioningAttributeSource == null) {
            return;
        }

        BasicAttributeBinding attributeBinding = doBasicSingularAttributeBindingCreation(
                versioningAttributeSource, entityBinding
        );
        entityBinding.getHierarchyDetails().setVersioningAttributeBinding(attributeBinding);
    }

    private void bindDiscriminator(RootEntitySource entitySource, EntityBinding entityBinding) {
        final DiscriminatorSource discriminatorSource = entitySource.getDiscriminatorSource();
        if (discriminatorSource == null) {
            return;
        }

        EntityDiscriminator discriminator = new EntityDiscriminator();
        SimpleValue relationalValue = makeSimpleValue(
                entityBinding,
                discriminatorSource.getDiscriminatorRelationalValueSource()
        );
        discriminator.setBoundValue(relationalValue);

        discriminator.getExplicitHibernateTypeDescriptor().setExplicitTypeName(
                discriminatorSource.getExplicitHibernateTypeName() != null
                        ? discriminatorSource.getExplicitHibernateTypeName()
                        : "string"
        );

        discriminator.setInserted(discriminatorSource.isInserted());
        discriminator.setForced(discriminatorSource.isForced());

        entityBinding.getHierarchyDetails().setEntityDiscriminator(discriminator);
        entityBinding.setDiscriminatorMatchValue(entitySource.getDiscriminatorMatchValue());
    }

    private void bindDiscriminatorValue(SubclassEntitySource entitySource, EntityBinding entityBinding) {
        final String discriminatorValue = entitySource.getDiscriminatorMatchValue();
        if (discriminatorValue == null) {
            return;
        }
        entityBinding.setDiscriminatorMatchValue(discriminatorValue);
    }

    private void bindAttributes(AttributeSourceContainer attributeSourceContainer, AttributeBindingContainer attributeBindingContainer) {
        // todo : we really need the notion of a Stack here for the table from which the columns come for binding these attributes.
        // todo : adding the concept (interface) of a source of attribute metadata would allow reuse of this method for entity, component, unique-key, etc
        // for now, simply assume all columns come from the base table....

        for (AttributeSource attributeSource : attributeSourceContainer.attributeSources()) {
            if (attributeSource.isSingular()) {
                final SingularAttributeSource singularAttributeSource = (SingularAttributeSource) attributeSource;
                if (singularAttributeSource.getNature() == SingularAttributeNature.COMPONENT) {
                    bindComponent((ComponentAttributeSource) singularAttributeSource, attributeBindingContainer);
                } else {
                    doBasicSingularAttributeBindingCreation(singularAttributeSource, attributeBindingContainer);
                }
            } else {
                bindPersistentCollection((PluralAttributeSource) attributeSource, attributeBindingContainer);
            }
        }
    }

    private void bindComponent(ComponentAttributeSource attributeSource, AttributeBindingContainer container) {
        final String attributeName = attributeSource.getName();
        SingularAttribute attribute = container.getAttributeContainer().locateComponentAttribute(attributeName);
        if (attribute == null) {
            final Component component = new Component(
                    attributeSource.getPath(),
                    attributeSource.getClassName(),
                    attributeSource.getClassReference(),
                    null // component inheritance not YET supported
            );
            attribute = container.getAttributeContainer().createComponentAttribute(attributeName, component);
        }
        ComponentAttributeBinding componentAttributeBinding = container.makeComponentAttributeBinding(attribute);

        if (StringHelper.isNotEmpty(attributeSource.getParentReferenceAttributeName())) {
            final SingularAttribute parentReferenceAttribute =
                    componentAttributeBinding.getComponent()
                            .createSingularAttribute(attributeSource.getParentReferenceAttributeName());
            componentAttributeBinding.setParentReference(parentReferenceAttribute);
        }

        componentAttributeBinding.setMetaAttributeContext(
                buildMetaAttributeContext(attributeSource.metaAttributes(), container.getMetaAttributeContext())
        );

        bindAttributes(attributeSource, componentAttributeBinding);
    }

    private void bindPersistentCollection(PluralAttributeSource attributeSource, AttributeBindingContainer attributeBindingContainer) {
        final PluralAttribute existingAttribute = attributeBindingContainer.getAttributeContainer()
                .locatePluralAttribute(attributeSource.getName());
        final AbstractPluralAttributeBinding pluralAttributeBinding;

        if (attributeSource.getPluralAttributeNature() == PluralAttributeNature.BAG) {
            final PluralAttribute attribute = existingAttribute != null
                    ? existingAttribute
                    : attributeBindingContainer.getAttributeContainer().createBag(attributeSource.getName());
            pluralAttributeBinding = attributeBindingContainer.makeBagAttributeBinding(
                    attribute,
                    convert(attributeSource.getElementSource().getNature())
            );
        } else if (attributeSource.getPluralAttributeNature() == PluralAttributeNature.SET) {
            final PluralAttribute attribute = existingAttribute != null
                    ? existingAttribute
                    : attributeBindingContainer.getAttributeContainer().createSet(attributeSource.getName());
            pluralAttributeBinding = attributeBindingContainer.makeSetAttributeBinding(
                    attribute,
                    convert(attributeSource.getElementSource().getNature())
            );
        } else {
            // todo : implement other collection types
            throw new NotYetImplementedException("Collections other than bag and set not yet implemented :(");
        }

        doBasicPluralAttributeBinding(attributeSource, pluralAttributeBinding);

        bindCollectionTable(attributeSource, pluralAttributeBinding);
        bindSortingAndOrdering(attributeSource, pluralAttributeBinding);

        bindCollectionKey(attributeSource, pluralAttributeBinding);
        bindCollectionElement(attributeSource, pluralAttributeBinding);
        bindCollectionIndex(attributeSource, pluralAttributeBinding);

        metadata.addCollection(pluralAttributeBinding);
    }

    private void doBasicPluralAttributeBinding(PluralAttributeSource source, AbstractPluralAttributeBinding binding) {
        binding.setFetchTiming(source.getFetchTiming());
        binding.setFetchStyle(source.getFetchStyle());
        binding.setCascadeStyles(source.getCascadeStyles());

        binding.setCaching(source.getCaching());

        binding.getHibernateTypeDescriptor().setJavaTypeName(
                source.getPluralAttributeNature().reportedJavaType().getName()
        );
        binding.getHibernateTypeDescriptor().setExplicitTypeName(source.getTypeInformation().getName());
        binding.getHibernateTypeDescriptor().getTypeParameters().putAll(source.getTypeInformation().getParameters());

        if (StringHelper.isNotEmpty(source.getCustomPersisterClassName())) {
            binding.setCollectionPersisterClass(
                    currentBindingContext.<CollectionPersister>locateClassByName(source.getCustomPersisterClassName())
            );
        }

        if (source.getCustomPersisterClassName() != null) {
            binding.setCollectionPersisterClass(
                    metadata.<CollectionPersister>locateClassByName(source.getCustomPersisterClassName())
            );
        }

        binding.setCustomLoaderName(source.getCustomLoaderName());
        binding.setCustomSqlInsert(source.getCustomSqlInsert());
        binding.setCustomSqlUpdate(source.getCustomSqlUpdate());
        binding.setCustomSqlDelete(source.getCustomSqlDelete());
        binding.setCustomSqlDeleteAll(source.getCustomSqlDeleteAll());

        binding.setMetaAttributeContext(
                buildMetaAttributeContext(
                        source.metaAttributes(),
                        binding.getContainer().getMetaAttributeContext()
                )
        );

        doBasicAttributeBinding(source, binding);
    }

    private CollectionLaziness interpretLaziness(String laziness) {
        if (laziness == null) {
            laziness = Boolean.toString(metadata.getMappingDefaults().areAssociationsLazy());
        }

        if ("extra".equals(laziness)) {
            return CollectionLaziness.EXTRA;
        } else if ("false".equals(laziness)) {
            return CollectionLaziness.NOT;
        } else if ("true".equals(laziness)) {
            return CollectionLaziness.LAZY;
        }

        throw new MappingException(
                String.format("Unexpected collection laziness value %s", laziness),
                currentBindingContext.getOrigin()
        );
    }

    private void bindCollectionTable(
            PluralAttributeSource attributeSource,
            AbstractPluralAttributeBinding pluralAttributeBinding) {
        if (attributeSource.getElementSource().getNature() == PluralAttributeElementNature.ONE_TO_MANY) {
            return;
        }

        final Schema.Name schemaName = org.hibernate.metamodel.source.hbm.Helper.determineDatabaseSchemaName(
                attributeSource.getExplicitSchemaName(),
                attributeSource.getExplicitCatalogName(),
                currentBindingContext
        );
        final Schema schema = metadata.getDatabase().locateSchema(schemaName);

        final String tableName = attributeSource.getExplicitCollectionTableName();
        if (StringHelper.isNotEmpty(tableName)) {
            final Identifier tableIdentifier = Identifier.toIdentifier(
                    currentBindingContext.getNamingStrategy().tableName(tableName)
            );
            Table collectionTable = schema.locateTable(tableIdentifier);
            if (collectionTable == null) {
                collectionTable = schema.createTable(tableIdentifier);
            }
            pluralAttributeBinding.setCollectionTable(collectionTable);
        } else {
            // todo : not sure wel have all the needed info here in all cases, specifically when needing to know the "other side"
            final EntityBinding owner = pluralAttributeBinding.getContainer().seekEntityBinding();
            final String ownerTableLogicalName = Table.class.isInstance(owner.getPrimaryTable())
                    ? Table.class.cast(owner.getPrimaryTable()).getTableName().getName()
                    : null;
            String collectionTableName = currentBindingContext.getNamingStrategy().collectionTableName(
                    owner.getEntity().getName(),
                    ownerTableLogicalName,
                    null,    // todo : here
                    null,    // todo : and here
                    pluralAttributeBinding.getContainer().getPathBase() + '.' + attributeSource.getName()
            );
            collectionTableName = quoteIdentifier(collectionTableName);
            pluralAttributeBinding.setCollectionTable(
                    schema.locateOrCreateTable(
                            Identifier.toIdentifier(
                                    collectionTableName
                            )
                    )
            );
        }

        if (StringHelper.isNotEmpty(attributeSource.getCollectionTableComment())) {
            pluralAttributeBinding.getCollectionTable().addComment(attributeSource.getCollectionTableComment());
        }

        if (StringHelper.isNotEmpty(attributeSource.getCollectionTableCheck())) {
            pluralAttributeBinding.getCollectionTable().addCheckConstraint(attributeSource.getCollectionTableCheck());
        }

        pluralAttributeBinding.setWhere(attributeSource.getWhere());
    }

    private void bindCollectionKey(
            PluralAttributeSource attributeSource,
            AbstractPluralAttributeBinding pluralAttributeBinding) {
        pluralAttributeBinding.getCollectionKey().prepareForeignKey(
                attributeSource.getKeySource().getExplicitForeignKeyName(),
                null  // todo : handle secondary table names
        );
        pluralAttributeBinding.getCollectionKey().getForeignKey().setDeleteRule(
                attributeSource.getKeySource().getOnDeleteAction()
        );
        // todo : need to bind "relational values", account for property-ref
    }

    private void bindCollectionElement(
            PluralAttributeSource attributeSource,
            AbstractPluralAttributeBinding pluralAttributeBinding) {
        final PluralAttributeElementSource elementSource = attributeSource.getElementSource();
        if (elementSource.getNature() == PluralAttributeElementNature.BASIC) {
            final BasicPluralAttributeElementSource basicElementSource = (BasicPluralAttributeElementSource) elementSource;
            final BasicCollectionElement basicCollectionElement = (BasicCollectionElement) pluralAttributeBinding.getCollectionElement();
            resolveTypeInformation(
                    basicElementSource.getExplicitHibernateTypeSource(),
                    pluralAttributeBinding.getAttribute(),
                    basicCollectionElement
            );
            // todo : temp
            return;
        }
        // todo : implement
        throw new NotYetImplementedException(
                String.format(
                        "Support for collection elements of type %s not yet implemented",
                        elementSource.getNature()
                )
        );
    }

    private void bindCollectionIndex(
            PluralAttributeSource attributeSource,
            AbstractPluralAttributeBinding pluralAttributeBinding) {
        if (attributeSource.getPluralAttributeNature() != PluralAttributeNature.LIST
                && attributeSource.getPluralAttributeNature() != PluralAttributeNature.MAP) {
            return;
        }

        // todo : implement
        throw new NotYetImplementedException();
    }

    private void bindSortingAndOrdering(
            PluralAttributeSource attributeSource,
            AbstractPluralAttributeBinding pluralAttributeBinding) {
        if (Sortable.class.isInstance(attributeSource)) {
            final Sortable sortable = Sortable.class.cast(attributeSource);
            if (sortable.isSorted()) {
                // todo : handle setting comparator

                // and then return because sorting and ordering are mutually exclusive
                return;
            }
        }

        if (Orderable.class.isInstance(attributeSource)) {
            final Orderable orderable = Orderable.class.cast(attributeSource);
            if (orderable.isOrdered()) {
                // todo : handle setting ordering
            }
        }
    }

    private void doBasicAttributeBinding(AttributeSource attributeSource, AttributeBinding attributeBinding) {
        attributeBinding.setPropertyAccessorName(attributeSource.getPropertyAccessorName());
        attributeBinding.setIncludedInOptimisticLocking(attributeSource.isIncludedInOptimisticLocking());
    }

    private CollectionElementNature convert(PluralAttributeElementNature pluralAttributeElementNature) {
        return CollectionElementNature.valueOf(pluralAttributeElementNature.name());
    }

    private BasicAttributeBinding doBasicSingularAttributeBindingCreation(
            SingularAttributeSource attributeSource,
            AttributeBindingContainer attributeBindingContainer) {
        final SingularAttribute existingAttribute = attributeBindingContainer.getAttributeContainer()
                .locateSingularAttribute(attributeSource.getName());
        final SingularAttribute attribute;
        if (existingAttribute != null) {
            attribute = existingAttribute;
        } else if (attributeSource.isVirtualAttribute()) {
            attribute = attributeBindingContainer.getAttributeContainer().createVirtualSingularAttribute(
                    attributeSource.getName()
            );
        } else {
            attribute = attributeBindingContainer.getAttributeContainer()
                    .createSingularAttribute(attributeSource.getName());
        }

        final BasicAttributeBinding attributeBinding;
        if (attributeSource.getNature() == SingularAttributeNature.BASIC) {
            attributeBinding = attributeBindingContainer.makeBasicAttributeBinding(attribute);
            resolveTypeInformation(attributeSource.getTypeInformation(), attributeBinding);
        } else if (attributeSource.getNature() == SingularAttributeNature.MANY_TO_ONE) {
            attributeBinding = attributeBindingContainer.makeManyToOneAttributeBinding(attribute);
            resolveTypeInformation(attributeSource.getTypeInformation(), attributeBinding);
            resolveToOneInformation(
                    (ToOneAttributeSource) attributeSource,
                    (ManyToOneAttributeBinding) attributeBinding
            );
        } else {
            throw new NotYetImplementedException();
        }

        attributeBinding.setGeneration(attributeSource.getGeneration());
        attributeBinding.setLazy(attributeSource.isLazy());
        attributeBinding.setIncludedInOptimisticLocking(attributeSource.isIncludedInOptimisticLocking());

        attributeBinding.setPropertyAccessorName(
                org.hibernate.metamodel.source.hbm.Helper.getPropertyAccessorName(
                        attributeSource.getPropertyAccessorName(),
                        false,
                        currentBindingContext.getMappingDefaults().getPropertyAccessorName()
                )
        );

        bindRelationalValues(attributeSource, attributeBinding);

        attributeBinding.setMetaAttributeContext(
                buildMetaAttributeContext(
                        attributeSource.metaAttributes(),
                        attributeBindingContainer.getMetaAttributeContext()
                )
        );

        return attributeBinding;
    }

    private void resolveTypeInformation(ExplicitHibernateTypeSource typeSource, BasicAttributeBinding attributeBinding) {
        final Class<?> attributeJavaType = determineJavaType(attributeBinding.getAttribute());
        if (attributeJavaType != null) {
            attributeBinding.getAttribute()
                    .resolveType(currentBindingContext.makeJavaType(attributeJavaType.getName()));
        }

        resolveTypeInformation(typeSource, attributeBinding.getHibernateTypeDescriptor(), attributeJavaType);
    }

    private void resolveTypeInformation(
            ExplicitHibernateTypeSource typeSource,
            PluralAttribute attribute,
            BasicCollectionElement collectionElement) {
        final Class<?> attributeJavaType = determineJavaType(attribute);
        resolveTypeInformation(typeSource, collectionElement.getHibernateTypeDescriptor(), attributeJavaType);
    }

    private void resolveTypeInformation(
            ExplicitHibernateTypeSource typeSource,
            HibernateTypeDescriptor hibernateTypeDescriptor,
            Class<?> discoveredJavaType) {
        if (discoveredJavaType != null) {
            hibernateTypeDescriptor.setJavaTypeName(discoveredJavaType.getName());
        }

        final String explicitTypeName = typeSource.getName();
        if (explicitTypeName != null) {
            final TypeDef typeDef = currentBindingContext.getMetadataImplementor()
                    .getTypeDefinition(explicitTypeName);
            if (typeDef != null) {
                hibernateTypeDescriptor.setExplicitTypeName(typeDef.getTypeClass());
                hibernateTypeDescriptor.getTypeParameters().putAll(typeDef.getParameters());
            } else {
                hibernateTypeDescriptor.setExplicitTypeName(explicitTypeName);
            }
            final Map<String, String> parameters = typeSource.getParameters();
            if (parameters != null) {
                hibernateTypeDescriptor.getTypeParameters().putAll(parameters);
            }
        } else {
            if (discoveredJavaType == null) {
                // we will have problems later determining the Hibernate Type to use.  Should we throw an
                // exception now?  Might be better to get better contextual info
            }
        }
    }

    /**
     * @param attribute the domain attribute
     * @return Returns the Java type of the attribute using reflection or {@code null} if the type cannot be discovered
     */
    private Class<?> determineJavaType(final SingularAttribute attribute) {
        try {
            final Class<?> ownerClass = attribute.getAttributeContainer().getClassReference();
            AttributeJavaTypeDeterminerDelegate delegate = new AttributeJavaTypeDeterminerDelegate(attribute.getName());
            BeanInfoHelper.visitBeanInfo(ownerClass, delegate);
            return delegate.javaType;
        } catch (Exception ignore) {
            // todo : log it?
        }
        return null;
    }

    private Class<?> determineJavaType(PluralAttribute attribute) {
        try {
            final Class<?> ownerClass = attribute.getAttributeContainer().getClassReference();
            PluralAttributeJavaTypeDeterminerDelegate delegate = new PluralAttributeJavaTypeDeterminerDelegate(
                    ownerClass,
                    attribute.getName()
            );
            BeanInfoHelper.visitBeanInfo(ownerClass, delegate);
            return delegate.javaType;
        } catch (Exception ignore) {
            // todo : log it?
        }
        return null;
    }

    private class PluralAttributeJavaTypeDeterminerDelegate implements BeanInfoHelper.BeanInfoDelegate {
        private final Class<?> ownerClass;
        private final String attributeName;

        private Class<?> javaType = null;

        private PluralAttributeJavaTypeDeterminerDelegate(Class<?> ownerClass, String attributeName) {
            this.ownerClass = ownerClass;
            this.attributeName = attributeName;
        }

        @Override
        public void processBeanInfo(BeanInfo beanInfo) throws Exception {
            for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
                if (propertyDescriptor.getName().equals(attributeName)) {
                    javaType = extractCollectionComponentType(beanInfo, propertyDescriptor);
                    break;
                }
            }
        }

        @SuppressWarnings({"unchecked"})
        private Class<?> extractCollectionComponentType(BeanInfo beanInfo, PropertyDescriptor propertyDescriptor) {
            final java.lang.reflect.Type collectionAttributeType;
            if (propertyDescriptor.getReadMethod() != null) {
                collectionAttributeType = propertyDescriptor.getReadMethod().getGenericReturnType();
            } else if (propertyDescriptor.getWriteMethod() != null) {
                collectionAttributeType = propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0];
            } else {
                // we need to look for the field and look at it...
                try {
                    collectionAttributeType = ownerClass.getField(propertyDescriptor.getName()).getGenericType();
                } catch (Exception e) {
                    return null;
                }
            }

            if (ParameterizedType.class.isInstance(collectionAttributeType)) {
                final java.lang.reflect.Type[] types = ((ParameterizedType) collectionAttributeType).getActualTypeArguments();
                if (types == null) {
                    return null;
                } else if (types.length == 1) {
                    return (Class<?>) types[0];
                } else if (types.length == 2) {
                    // Map<K,V>
                    return (Class<?>) types[1];
                }
            }
            return null;
        }
    }

    private void resolveToOneInformation(ToOneAttributeSource attributeSource, ManyToOneAttributeBinding attributeBinding) {
        final String referencedEntityName = attributeSource.getReferencedEntityName() != null
                ? attributeSource.getReferencedEntityName()
                : attributeBinding.getAttribute().getSingularAttributeType().getClassName();
        attributeBinding.setReferencedEntityName(referencedEntityName);
        // todo : we should consider basing references on columns instead of property-ref, which would require a resolution (later) of property-ref to column names
        attributeBinding.setReferencedAttributeName(attributeSource.getReferencedEntityAttributeName());

        attributeBinding.setCascadeStyles(attributeSource.getCascadeStyles());
        attributeBinding.setFetchTiming(attributeSource.getFetchTiming());
        attributeBinding.setFetchStyle(attributeSource.getFetchStyle());
    }

    private MetaAttributeContext buildMetaAttributeContext(EntitySource entitySource) {
        return buildMetaAttributeContext(
                entitySource.metaAttributes(),
                true,
                currentBindingContext.getMetadataImplementor().getGlobalMetaAttributeContext()
        );
    }

    private static MetaAttributeContext buildMetaAttributeContext(
            Iterable<MetaAttributeSource> metaAttributeSources,
            MetaAttributeContext parentContext) {
        return buildMetaAttributeContext(metaAttributeSources, false, parentContext);
    }

    private static MetaAttributeContext buildMetaAttributeContext(
            Iterable<MetaAttributeSource> metaAttributeSources,
            boolean onlyInheritable,
            MetaAttributeContext parentContext) {
        final MetaAttributeContext subContext = new MetaAttributeContext(parentContext);

        for (MetaAttributeSource metaAttributeSource : metaAttributeSources) {
            if (onlyInheritable & !metaAttributeSource.isInheritable()) {
                continue;
            }

            final String name = metaAttributeSource.getName();
            final MetaAttribute inheritedMetaAttribute = parentContext.getMetaAttribute(name);
            MetaAttribute metaAttribute = subContext.getLocalMetaAttribute(name);
            if (metaAttribute == null || metaAttribute == inheritedMetaAttribute) {
                metaAttribute = new MetaAttribute(name);
                subContext.add(metaAttribute);
            }
            metaAttribute.addValue(metaAttributeSource.getValue());
        }

        return subContext;
    }

    // Relational ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    private void bindPrimaryTable(EntitySource entitySource, EntityBinding entityBinding) {
        final TableSource tableSource = entitySource.getPrimaryTable();
        final Table table = createTable(entityBinding, tableSource);
        entityBinding.setPrimaryTable(table);
        entityBinding.setPrimaryTableName(table.getTableName().getName());
    }

    private void bindSecondaryTables(EntitySource entitySource, EntityBinding entityBinding) {
        for (TableSource secondaryTableSource : entitySource.getSecondaryTables()) {
            final Table table = createTable(entityBinding, secondaryTableSource);
            entityBinding.addSecondaryTable(secondaryTableSource.getLogicalName(), table);
        }
    }

    private Table createTable(EntityBinding entityBinding, TableSource tableSource) {
        String tableName = tableSource.getExplicitTableName();
        if (StringHelper.isEmpty(tableName)) {
            tableName = currentBindingContext.getNamingStrategy()
                    .classToTableName(entityBinding.getEntity().getClassName());
        } else {
            tableName = currentBindingContext.getNamingStrategy().tableName(tableName);
        }
        tableName = quoteIdentifier(tableName);

        final Schema.Name databaseSchemaName = org.hibernate.metamodel.source.hbm.Helper.determineDatabaseSchemaName(
                tableSource.getExplicitSchemaName(),
                tableSource.getExplicitCatalogName(),
                currentBindingContext
        );
        return currentBindingContext.getMetadataImplementor()
                .getDatabase()
                .locateSchema(databaseSchemaName)
                .locateOrCreateTable(Identifier.toIdentifier(tableName));
    }

    private void bindTableUniqueConstraints(EntitySource entitySource, EntityBinding entityBinding) {
        for (ConstraintSource constraintSource : entitySource.getConstraints()) {
            if (constraintSource instanceof UniqueConstraintSource) {
                TableSpecification table = entityBinding.locateTable(constraintSource.getTableName());
                if (table == null) {
                    // throw exception !?
                }
                String constraintName = constraintSource.name();
                if (constraintName == null) {
                    // create a default name
                }

                UniqueKey uniqueKey = table.getOrCreateUniqueKey(constraintName);
                for (String columnName : constraintSource.columnNames()) {
                    uniqueKey.addColumn(table.locateOrCreateColumn(quoteIdentifier(columnName)));
                }
            }
        }
    }

    private void bindRelationalValues(
            RelationalValueSourceContainer relationalValueSourceContainer,
            SingularAttributeBinding attributeBinding) {

        List<SimpleValueBinding> valueBindings = new ArrayList<SimpleValueBinding>();

        if (!relationalValueSourceContainer.relationalValueSources().isEmpty()) {
            for (RelationalValueSource valueSource : relationalValueSourceContainer.relationalValueSources()) {
                final TableSpecification table = attributeBinding.getContainer()
                        .seekEntityBinding()
                        .locateTable(valueSource.getContainingTableName());

                if (ColumnSource.class.isInstance(valueSource)) {
                    final ColumnSource columnSource = ColumnSource.class.cast(valueSource);
                    final Column column = makeColumn((ColumnSource) valueSource, table);
                    valueBindings.add(
                            new SimpleValueBinding(
                                    column,
                                    columnSource.isIncludedInInsert(),
                                    columnSource.isIncludedInUpdate()
                            )
                    );
                } else {
                    valueBindings.add(
                            new SimpleValueBinding(
                                    makeDerivedValue(((DerivedValueSource) valueSource), table)
                            )
                    );
                }
            }
        } else {
            String name = metadata.getOptions()
                    .getNamingStrategy()
                    .propertyToColumnName(attributeBinding.getAttribute().getName());
            name = quoteIdentifier(name);
            Column column = attributeBinding.getContainer()
                    .seekEntityBinding()
                    .getPrimaryTable()
                    .locateOrCreateColumn(name);
            column.setNullable(relationalValueSourceContainer.areValuesNullableByDefault());
            valueBindings.add(
                    new SimpleValueBinding(
                            column,
                            relationalValueSourceContainer.areValuesIncludedInInsertByDefault(),
                            relationalValueSourceContainer.areValuesIncludedInUpdateByDefault()
                    )
            );
        }
        attributeBinding.setSimpleValueBindings(valueBindings);
    }

    private String quoteIdentifier(String identifier) {
        return currentBindingContext.isGloballyQuotedIdentifiers() ? StringHelper.quote(identifier) : identifier;
    }

    private SimpleValue makeSimpleValue(
            EntityBinding entityBinding,
            RelationalValueSource valueSource) {
        final TableSpecification table = entityBinding.locateTable(valueSource.getContainingTableName());

        if (ColumnSource.class.isInstance(valueSource)) {
            return makeColumn((ColumnSource) valueSource, table);
        } else {
            return makeDerivedValue((DerivedValueSource) valueSource, table);
        }
    }

    private Column makeColumn(ColumnSource columnSource, TableSpecification table) {
        String name = columnSource.getName();
        name = metadata.getOptions().getNamingStrategy().columnName(name);
        name = quoteIdentifier(name);
        final Column column = table.locateOrCreateColumn(name);
        column.setNullable(columnSource.isNullable());
        column.setDefaultValue(columnSource.getDefaultValue());
        column.setSqlType(columnSource.getSqlType());
        column.setSize(columnSource.getSize());
        column.setDatatype(columnSource.getDatatype());
        column.setReadFragment(columnSource.getReadFragment());
        column.setWriteFragment(columnSource.getWriteFragment());
        column.setUnique(columnSource.isUnique());
        column.setCheckCondition(columnSource.getCheckCondition());
        column.setComment(columnSource.getComment());
        return column;
    }

    private DerivedValue makeDerivedValue(DerivedValueSource derivedValueSource, TableSpecification table) {
        return table.locateOrCreateDerivedValue(derivedValueSource.getExpression());
    }

    private void processFetchProfiles(EntitySource entitySource, EntityBinding entityBinding) {
        // todo : process the entity-local fetch-profile declaration
    }

    private static class AttributeJavaTypeDeterminerDelegate implements BeanInfoHelper.BeanInfoDelegate {
        private final String attributeName;
        private Class<?> javaType = null;

        private AttributeJavaTypeDeterminerDelegate(String attributeName) {
            this.attributeName = attributeName;
        }

        @Override
        public void processBeanInfo(BeanInfo beanInfo) throws Exception {
            for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
                if (propertyDescriptor.getName().equals(attributeName)) {
                    javaType = propertyDescriptor.getPropertyType();
                    break;
                }
            }
        }
    }
}
